<?php

/**
 * This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT License
 */

require_once 'phing/Task.php';
require_once 'task/AbstractPropelDataModelTask.php';
require_once 'builder/om/OMBuilder.php';
require_once 'builder/om/ClassTools.php';

/**
 * This Task converts the XML runtime configuration file into a PHP array for faster performance.
 *
 * @author     Hans Lellelid <hans@xmpl.org>
 * @package    propel.generator.task
 */
class PropelConvertConfTask extends AbstractPropelDataModelTask
{

	/**
	 * @var        PhingFile The XML runtime configuration file to be converted.
	 */
	private $xmlConfFile;

	/**
	 * @var        string This is the file where the converted conf array dump will be placed.
	 */
	private $outputFile;

	/**
	 * @var        string This is the file where the classmap manifest converted conf array dump will be placed.
	 */
	private $outputClassmapFile;

	/**
	 * [REQUIRED] Set the input XML runtime conf file.
	 * @param      PhingFile $v The XML runtime configuration file to be converted.
	 */
	public function setXmlConfFile(PhingFile $v)
	{
		$this->xmlConfFile = $v;
	}

	/**
	 * [REQUIRED] Set the output filename for the converted runtime conf.
	 * The directory is specified using AbstractPropelDataModelTask#setOutputDirectory().
	 * @param      string $outputFile
	 * @see        AbstractPropelDataModelTask#setOutputDirectory()
	 */
	public function setOutputFile($outputFile)
	{
		// this is a string, not a file
		$this->outputFile = $outputFile;
	}

	/**
	 * [REQUIRED] Set the output filename for the autoload classmap.
	 * The directory is specified using AbstractPropelDataModelTask#setOutputDirectory().
	 * @param      string $outputFile
	 * @see        AbstractPropelDataModelTask#setOutputDirectory()
	 */
	public function setOutputClassmapFile($outputFile)
	{
		// this is a string, not a file
		$this->outputClassmapFile = $outputFile;
	}

	/**
	 * The main method does the work of the task.
	 */
	public function main()
	{
		// Check to make sure the input and output files were specified and that the input file exists.

		if (!$this->xmlConfFile || !$this->xmlConfFile->exists()) {
			throw new BuildException("No valid xmlConfFile specified.", $this->getLocation());
		}

		if (!$this->outputFile) {
			throw new BuildException("No outputFile specified.", $this->getLocation());
		}

		$this->log('Loading XML configuration file...');
		
		// Create a PHP array from the runtime-conf.xml file
		$xmlDom = new DOMDocument();
		$xmlDom->load($this->xmlConfFile->getAbsolutePath());
		$xml = simplexml_load_string($xmlDom->saveXML());
		$phpconf = self::simpleXmlToArray($xml);
		$this->log(sprintf('Loaded "%s" successfully', $this->xmlConfFile->getAbsolutePath()), Project::MSG_VERBOSE);
		
		/* For some reason the array generated from runtime-conf.xml has separate
		 * 'log' section and 'propel' sections. To maintain backward compatibility
		 * we need to put 'log' back into the 'propel' section.
		 */
		$log = array();
		if (isset($phpconf['log'])) {
			$phpconf['propel']['log'] = $phpconf['log'];
			unset($phpconf['log']);
		}

		if(isset($phpconf['propel'])) {
			$phpconf = $phpconf['propel'];
		}
			
		// add generator version
		$phpconf['generator_version'] = $this->getGeneratorConfig()->getBuildProperty('version');
		
		if (!$this->outputClassmapFile) {
			// We'll create a default one for BC 
			$this->outputClassmapFile = 'classmap-' . $this->outputFile;
		} 
		
		// Write resulting PHP data to output file
		$outfile = new PhingFile($this->outputDirectory, $this->outputFile);
		$output = "<?php\n";
		$output .= "// This file generated by Propel " . $phpconf['generator_version'] . " convert-conf target".($this->getGeneratorConfig()->getBuildProperty('addTimestamp') ? " on " . strftime("%c") : '') . "\n";
		$output .= "// from XML runtime conf file " . $this->xmlConfFile->getPath() . "\n";
		$output .= "\$conf = ";
		$output .= var_export($phpconf, true);
		$output .= ";\n";
		$output .= "\$conf['classmap'] = include(dirname(__FILE__) . DIRECTORY_SEPARATOR . '".$this->outputClassmapFile."');\n";
		$output .= "return \$conf;";

		$mustWriteRuntimeConf = true;
		if (file_exists($outfile->getAbsolutePath())) {
			$currentRuntimeConf = file_get_contents($outfile->getAbsolutePath());
			if ($currentRuntimeConf == $output) {
				$this->log(sprintf('No change in PHP runtime conf file "%s"', $outfile->getPath()), Project::MSG_VERBOSE);
				$mustWriteRuntimeConf = false;
			} else {
				$this->log(sprintf('Updating PHP runtime conf file "%s"', $outfile->getPath()));
			}
		} else {
			$this->log(sprintf('Creating PHP runtime conf file "%s"', $outfile->getPath()));
		}
		if ($mustWriteRuntimeConf && !file_put_contents($outfile->getAbsolutePath(), $output)) {
			throw new BuildException("Error writing output file: " . $outfile->getAbsolutePath(), $this->getLocation());
		}
		
		// add classmap
		$phpconfClassmap = $this->getClassMap();
		$outfile = new PhingFile($this->outputDirectory, $this->outputClassmapFile);
		$output = '<' . '?' . "php\n";
		$output .= "// This file generated by Propel " . $phpconf['generator_version'] . " convert-conf target".($this->getGeneratorConfig()->getBuildProperty('addTimestamp') ? " on " . strftime("%c") : '') . "\n";
		$output .= "return ";
		$output .= var_export($phpconfClassmap, true);
		$output .= ";";
		
		$mustWriteClassMap = true;
		if (file_exists($outfile->getAbsolutePath())) {
			$currentClassmap = file_get_contents($outfile->getAbsolutePath());
			if ($currentClassmap == $output) {
				$this->log(sprintf('No change in PHP classmap file "%s"', $outfile->getPath()), Project::MSG_VERBOSE);
				$mustWriteClassMap = false;
			} else {
				$currentPhpconfClassmap = include $outfile->getAbsolutePath();
				if ($addedMaps = array_diff_assoc($phpconfClassmap, $currentPhpconfClassmap)) {
					$this->log('Added classmap for the following classes:', Project::MSG_VERBOSE);
					$this->logClassMap($addedMaps);
				}
				if ($removedMaps = array_diff_assoc($currentPhpconfClassmap, $phpconfClassmap)) {
					$this->log('Removed classmap for the following classes:', Project::MSG_VERBOSE);
					$this->logClassMap($removedMaps);
				}
				$this->log(sprintf('Updating PHP classmap file "%s"', $outfile->getPath()));
			}
		} else {
			$this->log(sprintf('Creating PHP classmap file "%s"', $outfile->getPath()));
			$this->logClassMap($phpconfClassmap);
		}
		if ($mustWriteClassMap && !file_put_contents($outfile->getAbsolutePath(), $output)) {
			throw new BuildException("Error writing output file: " . $outfile->getAbsolutePath(), $this->getLocation());
		}
		
		if (!$mustWriteRuntimeConf && !$mustWriteClassMap) {
			$this->log('No change in compiled configuration files');
		}

	} // main()

	protected function logClassMap($classMap)
	{
		foreach ($classMap as $className => $classPath) {
			$this->log(sprintf('  %-15s => %s', $className, $classPath), Project::MSG_VERBOSE);
		}
	}
	/**
	 * Recursive function that converts an SimpleXML object into an array.
	 * @author     Christophe VG (based on code form php.net manual comment)
	 * @param      object SimpleXML object.
	 * @return     array Array representation of SimpleXML object.
	 */
	private static function simpleXmlToArray($xml)
	{
		$ar = array();

		foreach ( $xml->children() as $k => $v ) {

			// recurse the child
			$child = self::simpleXmlToArray( $v );

			//print "Recursed down and found: " . var_export($child, true) . "\n";

			// if it's not an array, then it was empty, thus a value/string
			if ( count($child) == 0 ) {
				$child = self::getConvertedXmlValue($v);

			}

			// add the childs attributes as if they where children
			foreach ( $v->attributes() as $ak => $av ) {

				// if the child is not an array, transform it into one
				if ( !is_array( $child ) ) {
					$child = array( "value" => $child );
				}

				if ($ak == 'id') {
					// special exception: if there is a key named 'id'
					// then we will name the current key after that id
					$k = self::getConvertedXmlValue($av);
				} else {
					// otherwise, just add the attribute like a child element
					$child[$ak] = self::getConvertedXmlValue($av);
				}
			}

			// if the $k is already in our children list, we need to transform
			// it into an array, else we add it as a value
			if ( !in_array( $k, array_keys($ar) ) ) {
				$ar[$k] = $child;
			} else {
				// (This only applies to nested nodes that do not have an @id attribute)

				// if the $ar[$k] element is not already an array, then we need to make it one.
				// this is a bit of a hack, but here we check to also make sure that if it is an
				// array, that it has numeric keys.  this distinguishes it from simply having other
				// nested element data.

				if ( !is_array($ar[$k]) || !isset($ar[$k][0]) ) { $ar[$k] = array($ar[$k]); }
				$ar[$k][] = $child;
			}

		}

		return $ar;
	}

	/**
	 * Process XML value, handling boolean, if appropriate.
	 * @param      object The simplexml value object.
	 * @return     mixed
	 */
	private static function getConvertedXmlValue($value)
	{
		$value = (string) $value; // convert from simplexml to string
		// handle booleans specially
		$lwr = strtolower($value);
		if ($lwr === "false") {
			$value = false;
		} elseif ($lwr === "true") {
			$value = true;
		}
		return $value;
	}
	
	/**
	 * Lists data model classes and builds an associative array className => classPath
	 * To be used for autoloading
	 * @return array
	 */
	protected function getClassMap()
	{
		$phpconfClassmap = array();

		$generatorConfig = $this->getGeneratorConfig();

		foreach ($this->getDataModels() as $dataModel) {

			foreach ($dataModel->getDatabases() as $database) {

				$classMap = array();

				foreach ($database->getTables() as $table) {

					if (!$table->isForReferenceOnly()) {

						// -----------------------------------------------------
						// Add TableMap class,
						//     Peer, Object & Query stub classes,
						// and Peer, Object & Query base classes
						// -----------------------------------------------------
						// (this code is based on PropelOMTask)

						foreach (array('tablemap', 'peerstub', 'objectstub', 'querystub', 'peer', 'object', 'query') as $target) {
							$builder = $generatorConfig->getConfiguredBuilder($table, $target);
							$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
						}

						// -----------------------------------------------------
						// Add children classes for object and query,
						// as well as base child query,
						// for single tabel inheritance tables.
						// -----------------------------------------------------

						if ($col = $table->getChildrenColumn()) {
							if ($col->isEnumeratedClasses()) {
								foreach ($col->getChildren() as $child) {
									foreach (array('objectmultiextend', 'queryinheritance', 'queryinheritancestub') as $target) {
										$builder = $generatorConfig->getConfiguredBuilder($table, $target);
										$builder->setChild($child);
										$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
									}
								}
							}
						}
						
						// -----------------------------------------------------
						// Add base classes for alias tables (undocumented)
						// -----------------------------------------------------
						
						$baseClass = $table->getBaseClass();
						if ( $baseClass !== null ) {
							$className = ClassTools::classname($baseClass);
							if (!isset($classMap[$className])) {
								$classPath = ClassTools::getFilePath($baseClass);
								$classMap[$className] = $classPath;
							}
						}

						$basePeer = $table->getBasePeer();
						if ( $basePeer !== null ) {
							$className = ClassTools::classname($basePeer);
							if (!isset($classMap[$className])) {
								$classPath = ClassTools::getFilePath($basePeer);
								$classMap[$className] = $classPath;
							}
						}
						
						// ----------------------------------------------
						// Add classes for interface
						// ----------------------------------------------
						
						if ($table->getInterface()) {
							$builder = $generatorConfig->getConfiguredBuilder($table, 'interface');
							$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
						}
						
						// ----------------------------------------------
						// Add classes from old treeMode implementations
						// ----------------------------------------------

						if ($table->treeMode() == 'MaterializedPath') {
							foreach (array('nodepeerstub', 'nodestub', 'nodepeer', 'node') as $target) {
								$builder = $generatorConfig->getConfiguredBuilder($table, $target);
								$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
							}
						}
						if ($table->treeMode() == 'NestedSet') {
							foreach (array('nestedset', 'nestedsetpeer') as $target) {
								$builder = $generatorConfig->getConfiguredBuilder($table, $target);
								$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
							}
						}
						
						// ----------------------------------
						// Add classes added by behaviors
						// ----------------------------------
						if ($table->hasAdditionalBuilders()) {
							foreach ($table->getAdditionalBuilders() as $builderClass) {
								$builder = new $builderClass($table);
								$builder->setGeneratorConfig($generatorConfig);
								$classMap[$builder->getFullyQualifiedClassname()] = $builder->getClassFilePath();
							}
						}

					} // if (!$table->isReferenceOnly())
				}

				$phpconfClassmap = array_merge($phpconfClassmap, $classMap);
			}
		}
		
		// sort the classmap by class name, to avoid discrepancies between OS
		ksort($phpconfClassmap);
		
		return $phpconfClassmap;
	}
}
